#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv)
# Build and run a bots/image-customize command to prepare a VM for testing Cockpit.

# This file is part of Cockpit.
#
# Copyright (C) 2022 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <https://www.gnu.org/licenses/>.

import argparse
import os
import shlex
import shutil
import subprocess
import sys
from contextlib import contextmanager

from lib import testmap
from lib.constants import DEFAULT_IMAGE
from machine.machine_core import machine_virtual

BASE_DIR = os.path.realpath(f'{__file__}/../..')
TEST_DIR = f'{BASE_DIR}/test'
BOTS_DIR = f'{BASE_DIR}/bots'


@contextmanager
def create_machine(image):
    subprocess.check_call([os.path.join(BOTS_DIR, "image-download"), image])
    network = machine_virtual.VirtNetwork(image=image)
    machine = machine_virtual.VirtMachine(image=image, networking=network.host(restrict=True))
    try:
        machine.start()
        machine.wait_boot()
        yield machine
    finally:
        machine.stop()


def build_rpms(dist_tar, machine, verbose, quick):
    """build RPMs from a tarball in an machine

    Return local rpm path list.
    """
    vm_tar = os.path.join("/var/tmp", os.path.basename(dist_tar))
    machine.upload([dist_tar], vm_tar)

    # build srpm
    machine.execute(fr"""
        su builder -c 'rpmbuild --define "_topdir /var/tmp/build" -ts "{vm_tar}"'
    """)

    # build rpms
    mock_opts = ("--verbose" if verbose else "") + (" --nocheck" if quick else "")
    machine.execute(fr"""
        su builder -c 'mock --no-clean --disablerepo=* --offline \
                            --resultdir /var/tmp/build {mock_opts} \
                            --rebuild /var/tmp/build/SRPMS/*.src.rpm'
    """, timeout=1800)

    # download rpms
    vm_rpms = machine.execute("find /var/tmp/build -name '*.rpm' -not -name '*.src.rpm'").strip().split()

    destdir = os.path.abspath("tmp/rpms")
    if os.path.exists(destdir):
        shutil.rmtree(destdir)
    os.makedirs(destdir)

    rpms = []
    for rpm in vm_rpms:
        machine.download(rpm, destdir)
        rpms.append(os.path.join(destdir, os.path.basename(rpm)))
    return rpms


#
# Helper functions to build image-customize options for various steps
#

def build_install_package(dist_tar, image):
    """Default rpm/deb/arch package build/install"""

    # our images have distro cockpit packages pre-installed, remove them
    args = ["--run-command"]
    if 'debian' in image or 'ubuntu' in image:
        # set up pbuilder hook to make install-tests
        cmd = """
        set -ex
        dpkg --purge cockpit cockpit-ws cockpit-bridge cockpit-system
        mkdir -p /var/tmp/tests
        echo 'HOOKDIR="/etc/pbuilder-hooks"' >> /etc/pbuilderrc
        echo 'BINDMOUNTS="/var/tmp/tests"' >> /etc/pbuilderrc
        """
        args.append(cmd)
        args += ["--upload", os.path.join(TEST_DIR, "pbuilder-hooks") + ":/etc/"]
    else:
        # subscription-manager-cockpit needs these, thus --nodeps
        args.append(r"""
            if rpm -q cockpit-ws >/dev/null 2>&1
                then rpm --erase --nodeps --verbose cockpit cockpit-ws cockpit-bridge cockpit-system
            fi
        """)

    args += ["--build", dist_tar]

    # install the playground
    args += ["--upload", os.path.join(BASE_DIR, "dist/playground") + ":/usr/local/share/cockpit/"]

    # install mock pam module; we don't want to ship it as package, so fish it out of the build root
    args.append("--run-command")
    if 'debian' in image or 'ubuntu' in image:
        args.append(r"find /var/tmp/tests -name mock-pam-conv-mod.so -exec cp {} /lib/x86_64-linux-gnu/security \;")
    elif 'arch' in image:
        args.append(r"find /var/lib/archbuild -name mock-pam-conv-mod.so -exec cp {} /usr/lib/security/ \;")
    else:
        args.append(r"find /var/lib/mock -name mock-pam-conv-mod.so -exec cp {} /usr/lib64/security/ \;")

    if 'debian' in image or 'ubuntu' in image:
        args.append("--run-command")
        suppress = {'initial-upload-closes-no-bugs', 'newer-standards-version'}
        # older lintian not yet complain about pkg/static/login.html and src/common/fail.html
        if image in ["ubuntu-2204"]:
            suppress.add("mismatched-override")
        # Ubuntu 22.04 raises elf-error on *-dbgsym: "In program headers: Unable to find program interpreter name"
        if image == "ubuntu-2204":
            suppress.add("elf-error")

        args.append(fr"""
            cd /var/tmp/build;
            runuser -u admin -- \
                lintian --fail-on warning,error \
                --tag-display-limit 0 --display-info \
                --suppress-tags {','.join(suppress)} \
                cockpit*.changes >&2""")
    return args


def build_install_ostree(dist_tar, image, verbose, quick):
    """Special treatment of build/install on CoreOS

    OSTree image can't build packages, build them on corresponding OSes and
    install them into the OSTree and the cockpit/ws container.
    """
    with create_machine(testmap.get_build_image(image)) as machine:
        rpms = build_rpms(dist_tar, machine, verbose, quick)
    args = []
    for rpm in rpms:
        args += ["--upload", f"{rpm}:/var/tmp/"]
    args += [
        "--upload", os.path.join(BASE_DIR, "containers") + ":/var/tmp/",
        "--script", os.path.join(TEST_DIR, "ws-container.install"),
        "--script", os.path.join(TEST_DIR, "ostree.install"),
        "--upload", os.path.join(BASE_DIR, "dist/playground") + ":/usr/local/share/cockpit/"]
    return args


def build_install_bootc(dist_tar, image, verbose, quick):
    """Special treatment of build/install on Bootc

    bootc image can't build packages, build them on corresponding OSes, build a new bootc
    image with the rpms, and deploy it.
    """
    with create_machine(testmap.get_build_image(image)) as machine:
        rpms = build_rpms(dist_tar, machine, verbose, quick)
    args = ["--run-command", "mkdir -p /var/tmp/rpms"]
    for rpm in rpms:
        if not any(r in rpm for r in [".src", "debug", "packagekit"]):
            args += ["--upload", f"{rpm}:/var/tmp/rpms/"]
    args += [
        "--upload", os.path.join(BASE_DIR, "dist/playground") + ":/var/tmp/",
        "--script", os.path.join(TEST_DIR, "bootc.install"),
    ]
    return args


def build_install_container(dist_tar, image, verbose, quick):
    """Build VM with cockpit/ws container

    This can test an image with beibooting from a cockpit/ws container, without
    any installed cockpit packages.
    """
    # this needs to be the image that corresponds to the actual cockpit/ws container
    with open(os.path.join(BASE_DIR, "containers/ws/Containerfile")) as f:
        for line in f:
            if line.startswith("FROM"):
                fedora_version = line.split('fedora:')[1].split()[0]
                break
        else:
            raise ValueError("failed to parse Fedora version from ws container")

    with create_machine(f"fedora-{fedora_version}") as m:
        build_rpms(dist_tar, m, verbose, quick)
        # copy the rpms and playground where ws-container.install expects them
        m.execute(r"find /var/tmp/build -name '*.rpm' -not -name '*.src.rpm' -exec mv {} /var/tmp/ \;")
        m.execute("mkdir -p /var/tmp/install/usr/share/cockpit/")
        m.upload([os.path.join(BASE_DIR, "dist/playground")], "/var/tmp/install/usr/share/cockpit/")
        # create container
        m.upload([os.path.join(BASE_DIR, "containers")], "/var/tmp/")
        with open(os.path.join(TEST_DIR, "ws-container.install")) as f:
            m.execute(f.read())
        # download container
        m.execute("podman save cockpit/ws -o /var/tmp/ws.tar")
        m.download("/var/tmp/ws.tar", os.path.abspath("tmp/"))

    return [
        # install container
        "--upload", os.path.abspath("tmp/ws.tar") + ":/var/tmp/",
        "--run-command", "podman load -i /var/tmp/ws.tar; rm /var/tmp/ws.tar",
        # remove preinstalled rpms
        "--run-command", "dnf -C remove -y cockpit-bridge cockpit-ws",
    ]


def validate_packages():
    """Post-install package checks"""

    # check for files that are shipped by more than one RPM
    return ["--run-command",
            """set -eu
                fail=
                for f in $(find $(rpm -ql $(rpm -qa '*cockpit*') | sort | uniq -d) -maxdepth 0 -type f); do
                    # -debugsource overlap is legit
                    [ "${f#/usr/src/debug}" = "$f" ] || continue
                    echo "ERROR: $f is shipped by multiple packages: $(rpm -qf $f)" >&2
                    fail=1
                done
                [ -z "${fail}" ] || exit 1
            """]


def main():
    parser = argparse.ArgumentParser(
        description='Prepare testing environment, download images and build and install cockpit',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('-e', '--env', metavar='NAME=VAL', action='append',
                        help="Add a line to /etc/environment")
    parser.add_argument('-d', '--debug', action='append_const', dest='env', const='COCKPIT_DEBUG=all',
                        help="Equivalent to --env=COCKPIT_DEBUG=all")
    parser.add_argument('-v', '--verbose', action='store_true', help='Display verbose progress details')
    parser.add_argument('-q', '--quick', action='store_true', help='Skip unit tests to build faster')
    parser.add_argument('-o', '--overlay', action='store_true',
                        help='Install into existing test/image/ overlay instead of from pristine base image')
    parser.add_argument('--network', action='store_true',
                        help='Enable internet access (only for local experiments, not for production!)')
    parser.add_argument('--container', action='store_true', help='Install cockpit/ws container instead of rpms')
    parser.add_argument('image', nargs='?', default=DEFAULT_IMAGE, help='The image to use')
    args = parser.parse_args()

    customize = [os.path.join(BOTS_DIR, "image-customize")]
    if not args.network:
        customize.append("--no-network")
    if not args.overlay:
        customize.append("--fresh")
    if args.verbose:
        customize.append("--verbose")
    if args.quick:
        customize.append("--quick")

    dist_tar = subprocess.check_output([f'{BASE_DIR}/tools/make-dist'], text=True).strip()

    if args.image == "fedora-coreos":
        customize += build_install_ostree(dist_tar, args.image, args.verbose, args.quick)
    elif args.image.endswith("-bootc"):
        customize += build_install_bootc(dist_tar, args.image, args.verbose, args.quick)
    elif args.container:
        customize += build_install_container(dist_tar, args.image, args.verbose, args.quick)
    else:
        customize += build_install_package(dist_tar, args.image)

    if not args.quick:
        customize += validate_packages()

    # post build/install test preparation
    customize += ["--script", os.path.join(TEST_DIR, "vm.install")]

    if args.env:
        customize += ['--run-command', r'printf "%s\n" >>/etc/environment ' + shlex.join(args.env)]

    customize.append(args.image)

    # show final command for easy copy&paste reproduction/debugging
    if args.verbose:
        print(' '.join([shlex.quote(arg) for arg in customize]))

    return subprocess.call(customize)


if __name__ == "__main__":
    sys.exit(main())
